package com.feicheng.wxpay.app;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import com.alibaba.fastjson.JSONObject;
import com.feicheng.common.constant.Constant;
import com.feicheng.common.exception.SystemException;
import com.feicheng.common.wx.pay.WxPayBean;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferRequest;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class WxAppPayBean extends WxPayBean {

	private String appId;

	private String secret;

	private String payUrl;

	/** 商户号 */
	private String merchantId;

	/** 商户API私钥路径 */
	private String privateKeyPath;

	/** 商户API私钥内容 */
	private String privateKeyStr;

	/** 商户证书序列号 */
	private String merchantSerialNumber;

	/** 商户APIV3密钥 */
	private String apiV3key = "";

	private static Map<String, Config> CONFIG_MAP = new HashMap<String, Config>();

	private JsapiServiceExtension service;

	private TransferBatchService tService;
	
	private RefundService rService;

	private Random random = new Random();

	public String getMerchantId() {
		return this.merchantId;
	}
	
	public Config getConfig() {
		return CONFIG_MAP.get(this.merchantId);
	}

	public WxAppPayBean(String appId, String secret, String merchantId, String privateKeyStr,
			String merchantSerialNumber, String apiV3key, String payUrl) {
		this.appId = appId;
		this.secret = secret;
		this.payUrl = payUrl;
		this.merchantId = merchantId;
		this.privateKeyStr = privateKeyStr;
		this.merchantSerialNumber = merchantSerialNumber;
		this.apiV3key = apiV3key;
		
		Config config = null;
		
		synchronized (WxAppPayBean.class) {
			
			config = CONFIG_MAP.get(merchantId);

			if (null == config) {
				config = new RSAAutoCertificateConfig.Builder().merchantId(merchantId).privateKey(privateKeyStr)
						.merchantSerialNumber(merchantSerialNumber).apiV3Key(apiV3key).build();
				CONFIG_MAP.put(merchantId, config);
				
			}
		}

		service = new JsapiServiceExtension.Builder().config(config).build();
		tService = new TransferBatchService.Builder().config(config).build();
		rService = new RefundService.Builder().config(config).build();

	}

	/**
	 * 生成随机数
	 *
	 * @return
	 */
	private String getNonceStr() {
		return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
	}

	/**
	 * 拼接参数
	 *
	 * @return
	 */
	private String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
		return appId + "\n" + timestamp + "\n" + nonceStr + "\n" + packag + "\n";
	}

	/**
	 * 获取私钥
	 *
	 * @param key 私钥内容 (required)
	 * @return 私钥对象
	 */
	public PrivateKey getPrivateKey(String key) throws IOException {
		try {
			String privateKey = key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "")
					.replaceAll("\\s+", "");

			KeyFactory kf = KeyFactory.getInstance("RSA");
			return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
		} catch (NoSuchAlgorithmException e) {
			throw new SystemException("当前Java环境不支持RSA", e);
		} catch (InvalidKeySpecException e) {
			throw new SystemException("无效的密钥格式");
		}
	}

	/**
	 * 生成签名
	 *
	 * @return
	 */
	private String sign(byte[] message)
			throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
		Signature sign = Signature.getInstance("SHA256withRSA"); // SHA256withRSA
		sign.initSign(this.getPrivateKey(this.privateKeyStr));
		sign.update(message);
		return Base64.getEncoder().encodeToString(sign.sign());
	}

	public Transaction parseNotice(String nonce, String signature, String signatureType, String timestamp,
			String serial, String body) {
		RequestParam requestParam = new RequestParam.Builder().serialNumber(serial).nonce(nonce).signature(signature)
				.timestamp(timestamp).signType(signatureType).body(body).build();

		return new NotificationParser((NotificationConfig) this.getConfig()).parse(requestParam, Transaction.class);
	}
	
	public RefundNotification parseRefundNotice(String nonce, String signature, String signatureType, String timestamp,
			String serial, String body) {
		RequestParam requestParam = new RequestParam.Builder().serialNumber(serial).nonce(nonce).signature(signature)
				.timestamp(timestamp).signType(signatureType).body(body).build();

		return new NotificationParser((NotificationConfig) this.getConfig()).parse(requestParam, RefundNotification.class);
	}

	/**
	 * @param outTradeNo  本地订单编号 （payment_record -> pay_id
	 * @param description 描述
	 * @param notifyUrl   回调地址
	 * @param amount      金额
	 * @param openId      客户的openId
	 * @return prepayId 预支付交易单ID
	 */
	public PrepayWithRequestPaymentResponse jsapi(String outTradeNo, String description, String notifyUrl,
			Integer amount, String openId) {
		try {
			// 跟之前下单示例一样，填充预下单参数
			PrepayRequest request = new PrepayRequest();

			Amount amountObj = new Amount();
			amountObj.setTotal(amount);
			request.setAmount(amountObj);
			request.setAppid(this.appId);
			request.setMchid(this.merchantId);
			request.setDescription(description);
			request.setNotifyUrl(this.payUrl + notifyUrl);
			request.setOutTradeNo(outTradeNo);
			Payer payer = new Payer();
			payer.setOpenid(openId);
			request.setPayer(payer);

			// response包含了调起支付所需的所有参数，可直接用于前端调起支付
			return service.prepayWithRequestPayment(request);
		} catch (Exception e) {
			log.error("支付失败：统一预付单失败", e);
			throw new SystemException("支付失败：统一预付单失败");
		}
	}

	/**
	 * 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml 计算签名值
	 * TODO 版本升级 准备删除
	 * 
	 * @param prepay_id
	 * @return
	 * @throws IOException
	 * @throws SignatureException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 */
	private HashMap<String, Object> getTokenJSAPI(String prepay_id) {
		HashMap<String, Object> map = new HashMap<String, Object>();

		try {
			// 获取随机字符串
			String nonceStr = this.getNonceStr();
			// 获取支付package
			String packagestr = "prepay_id=" + prepay_id;
			long timestamp = System.currentTimeMillis() / 1000;
			// 签名，使用字段appId、timeStamp、nonceStr、package计算得出的签名值
			String message = this.buildMessageTwo(this.appId, timestamp, nonceStr, packagestr);
			// 获取对应的签名
			String signature = this.sign(message.getBytes("utf-8"));
			// 组装返回

			map.put("appId", appId);
			map.put("timeStamp", String.valueOf(timestamp));
			map.put("nonceStr", nonceStr);
			map.put("package", packagestr);
			map.put("signType", "RSA");
			map.put("paySign", signature);

		} catch (NoSuchAlgorithmException | SignatureException | IOException | InvalidKeyException e) {
			throw new SystemException("唤起支付准备出现问题", e);
		}

		return map;
	}

	private String getRandomString(int length) {
		String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
		StringBuffer sb = new StringBuffer();

		for (int i = 0; i < length; ++i) {
			int number = random.nextInt(62);
			sb.append(str.charAt(number));
		}

		return sb.toString();
	}

	private String createOutTradeNo(String otNHeader, Integer storeId, Integer clientId, Integer payId) {
		StringBuilder sbResult = new StringBuilder(otNHeader);
		sbResult.append(storeId).append("_").append(clientId).append("_").append(payId).append("_");
		String resultTemp = sbResult.toString();
		if (resultTemp.length() < 32) {
			int tempLength = 32 - resultTemp.length();
			sbResult.append(this.getRandomString(tempLength));
		}

		return sbResult.toString();
	}

	public String createOutTradeNoForDepositIn(Integer storeId, Integer clientId, Integer payId) {
		return createOutTradeNo("DI-", storeId, clientId, payId);
	}

	public String createOutTradeNoForDepositOut(Integer storeId, Integer clientId, Integer payId) {
		return createOutTradeNo("DO-", storeId, clientId, payId);
	}

	public String createOutTradeNoForOrder(Integer orderId, Integer storeId, Integer clientId, Integer payId) {
		return createOutTradeNo(new StringBuilder("OI_").append(orderId).append("-").toString(), storeId, clientId,
				payId);
	}

	public String createOutTradeNoForCardIn(Integer storeId, Integer clientId, Integer payId) {
		return createOutTradeNo(new StringBuilder("CI").append("-").toString(), storeId, clientId, payId);
	}

	public JSONObject releaseOutTradeNo(String outTradeNo) {
		JSONObject result = new JSONObject();

		String[] otNArray = outTradeNo.split("-");

		String otNHead = otNArray[0];
		String otNData = otNArray[1];
		String[] bIdArray = otNHead.split("_");
		if (outTradeNo.startsWith("OI")) {
			result.put(Constant.ENTITY.ORDER.ORDER_ID, Integer.valueOf(bIdArray[1]));
		}

		String[] idArray = otNData.split("_");
		result.put(Constant.ENTITY.STORE.STORE_ID, Integer.valueOf(idArray[0]));
		result.put(Constant.ENTITY.CLIENT.CLIENT_ID, Integer.valueOf(idArray[1]));
		result.put(Constant.ENTITY.PAYMENT_RECORD.PAY_ID, Integer.valueOf(idArray[2]));
		return result;
	}

	public InitiateBatchTransferResponse depositTrans(InitiateBatchTransferRequest request) {
		return tService.initiateBatchTransfer(request);
	}

	public Refund refund(CreateRequest request) {
		try {
			request.setNotifyUrl(this.payUrl + request.getNotifyUrl());
			System.out.println(request.getNotifyUrl());
			return rService.create(request);
		} catch (HttpException e) { // 发送HTTP请求失败
			// 调用e.getHttpRequest()获取请求打印日志或上报监控，更多方法见HttpException定义
			throw new SystemException("退款：发送HTTP请求失败", e);
		} catch (ServiceException e) { // 服务返回状态小于200或大于等于300，例如500
			// 调用e.getResponseBody()获取返回体打印日志或上报监控，更多方法见ServiceException定义
			throw new SystemException("退款：发送HTTP请求失败");
		} catch (MalformedMessageException e) { // 服务返回成功，返回体类型不合法，或者解析返回体失败
			// 调用e.getMessage()获取信息打印日志或上报监控，更多方法见MalformedMessageException定义
			throw new SystemException("退款：返回体类型不合法，或者解析返回体失败");
		}
	}

}
